/**
 * Copyright (c) 2009 Blue Whale Systems Ltd
 * 
 * This software is provided "as is," and the copyright holder makes no representations or warranties, express or
 * implied, including but not limited to warranties of merchantability or fitness for any particular purpose or that the
 * use of this software or documentation will not infringe any third party patents, copyrights, trademarks or other
 * rights.
 * 
 * The copyright holder will not be liable for any direct, indirect special or consequential damages arising out of any
 * use of this software or documentation.
 */
package com.sun.lwuit;

import java.util.Vector;

import com.sun.lwuit.plaf.Style;

public class WrappingTextArea extends TextArea
{
    public void setIndicateErrorsInWrappingLogic( boolean aShouldIndicateErrors )
    {
        iShouldIndicateErrors = aShouldIndicateErrors;
    }

    public boolean haveErrorsInWrappingLogicOccurred()
    {
        return iShouldIndicateErrors && iErrorsOccurred;
    }

    // TextArea

    protected void initRowString()
    {
        // This should be done first so even if there's an error (expected or not) the row strings are there.
        rowStrings = new Vector();
        
        Style style = getStyle();
        widthForRowCalculations = getWidth() - style.getPadding( RIGHT ) - style.getPadding( LEFT );

        String text = getText();

        // If there's no width for text, then there are no rows. Easy-peasy, lemon squeezy.
        if( 0 >= widthForRowCalculations || null == text || 0 == text.length() )
        {
            return;
        }

        Font font = style.getFont();

        if( isSingleLineTextArea() )
        {
            char[] chars = text.toCharArray();

            if( font.charsWidth( chars, 0, chars.length ) < widthForRowCalculations )
            {
                replaceUnprintableCharacters( chars, 0, chars.length );
                rowStrings.addElement( new String( chars ) );
            }
            else
            {
                // Find a shorter substring that fits in a single row and, for cosmetic purposes, put '...' at the end.
                int delta = getBruteCountForSingleRow( chars, 0, chars.length, widthForRowCalculations, font );

                if( MINIMUM_LENGTH_BEFORE_INSERTING_ELLIPSIS < delta )
                {
                    if( false )
                    {
                        // This usually does not work. The built-in fonts do not have extended ascii support it seems.
                        chars[delta - 1] = ASCII_ELLIPSIS;
                    }
                    else
                    {
                        chars[delta - 1] = '.';
                        chars[delta - 2] = '.';
                        chars[delta - 3] = '.';
                    }
                }

                replaceUnprintableCharacters( chars, 0, delta );
                String line = new String( chars, 0, delta );
                rowStrings.addElement( line );
            }
        }
        else
        {
            textToRows( text.toCharArray(), 0, widthForRowCalculations, font );
        }
    }

    /**
     * Divides the text content into a list of substrings, each of which is no wider than the provided width.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     */
    private void textToRows( char[] aChars, int aOffset, int aWidth, Font aFont )
    {
        int delta = 0;
        int length = aChars.length;
        int lastSoftBreakOffset = -1;
        String line = null;

        iErrorsOccurred = false;

        try
        {
            while( aOffset + delta < length )
            {
                switch( aChars[aOffset + delta] )
                {
                    case ASCII_CR:
                    case ASCII_TAB:
                        aChars[aOffset + delta] = ASCII_SPACE;
                        // Fall-through intentionally...

                    case ASCII_SPACE:
                    case '.':
                    case '-':
                    case '/':
                    case '\\':
                    case '_':
                    case '@':
                    case '?':
                    case '=':
                    case '&':
                        if( aFont.charsWidth( aChars, aOffset, delta ) < aWidth )
                        {
                            // Reset the 'soft' line break marker with a new value.
                            lastSoftBreakOffset = delta;
                        }
                        else
                        {
                            // We could wait till the next 'hard' line break (or indeed, EOF) but since we already know
                            // that the substring does not fit, trigger the code that normally occurs on 'hard' breaks.
                            if( -1 != lastSoftBreakOffset )
                            {
                                lastSoftBreakOffset++;
                                line = new String( aChars, aOffset, lastSoftBreakOffset );
                                rowStrings.addElement( line );
                                aOffset += lastSoftBreakOffset;
                                lastSoftBreakOffset = -1;
                                delta = 0;
                            }
                            else
                            {
                                bruteTextToRows( aChars, aOffset, delta, aWidth, aFont );
                                aOffset += delta;
                                delta = 0;
                            }
                        }

                        delta++;
                        break;

                    case ASCII_LF:
                        if( aFont.charsWidth( aChars, aOffset, delta ) < aWidth )
                        {
                            // The 'normal' situation, a 'hard' line break with all the accumulated content fitting
                            // nicely into a single row.
                            aChars[aOffset + delta] = ASCII_SPACE;
                            line = new String( aChars, aOffset, delta );
                            rowStrings.addElement( line );
                            aOffset += delta;
                        }
                        else
                        {
                            // Use the most recent 'soft' line break position or brute-divide the substring.
                            if( -1 != lastSoftBreakOffset )
                            {
                                lastSoftBreakOffset++;
                                line = new String( aChars, aOffset, lastSoftBreakOffset );
                                rowStrings.addElement( line );
                                aOffset += lastSoftBreakOffset;
                                lastSoftBreakOffset = -1;
                            }
                            else
                            {
                                aChars[aOffset + delta] = ASCII_SPACE;
                                bruteTextToRows( aChars, aOffset, delta, aWidth, aFont );
                                aOffset += delta;
                            }
                        }

                        delta = 0;
                        break;

                    default:
                        delta++;
                        break;
                }
            }

            if( aOffset < length )
            {
                // This logic runs pretty much every time because there's usually going to be some 'tail' substring. In 
                // the extreme case when neither a 'hard' nor a 'soft' line break is found, all the text content will be
                // processed here.
                if( aFont.charsWidth( aChars, aOffset, length - aOffset ) < aWidth )
                {
                    line = new String( aChars, aOffset, length - aOffset );
                    rowStrings.addElement( line );
                }
                else
                {
                    if( -1 != lastSoftBreakOffset )
                    {
                        lastSoftBreakOffset++;
                        line = new String( aChars, aOffset, lastSoftBreakOffset );
                        rowStrings.addElement( line );
                        textToRows( aChars, aOffset + lastSoftBreakOffset, aWidth, aFont );
                    }
                    else
                    {
                        bruteTextToRows( aChars, aOffset, length - aOffset, aWidth, aFont );
                    }
                }
            }
        }
        catch( RuntimeException re )
        {
            // Catch and print a little debugging info before re-throwing.
            // See ticket:3511 Unable to open viewer -- WrappingTextError problem with Facebook emails
            System.out.print( "WrappingTextArea.textToRows in back-recursion aOffset[" + aOffset + "] length[" + length + "], delta[" + delta + "] aChars:[" + new String( aChars ) + "]" );

            // Record that on this run an error occurred. In addition, if error-indication is turned on, do not 
            // rethrow the exception but rather show what's been processed in a different text color.
            iErrorsOccurred = true;

            if( !iShouldIndicateErrors )
            {
                throw re;
            }
        }
    }

    /**
     * Generates one or more rows out of a substring when neither a 'soft' nor a 'hard' line break is found.
     * 
     * Note: This method assumes that the provided substring does NOT fit fully in a single row.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aCount length of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     */
    private void bruteTextToRows( char[] aChars, int aOffset, int aCount, int aWidth, Font aFont )
    {
        int count = getBruteCountForSingleRow( aChars, aOffset, aCount, aWidth, aFont );
        int offset = aOffset + count;
        int finalCount = aCount - count;

        String line = new String( aChars, aOffset, count );
        rowStrings.addElement( line );

        if( offset < aOffset + aCount )
        {
            if( aFont.charsWidth( aChars, offset, finalCount ) < aWidth )
            {
                line = new String( aChars, offset, finalCount );
                rowStrings.addElement( line );
            }
            else
            {
                bruteTextToRows( aChars, offset, finalCount, aWidth, aFont );
            }
        }
    }

    /**
     * Locates the position of the last character to be shown on any particular line when neither a 'soft' 
     * nor a 'hard' line break is found.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aCount length of the substring to consider
     * @param aWidth width available to draw the substring
     * @param aFont font used to draw the substring
     * @return the delta (position) of the last character in the substring that fits on a single row
     */
    private int getBruteCountForSingleRow( char[] aChars, int aOffset, int aCount, int aWidth, Font aFont )
    {
        int count = 0;
        int width = -1;

        do
        {
            count += DELTA_FOR_SINGLE_ROW_COUNT;

            if( count >= aCount )
            {
                count = aCount;
            }

            width = aFont.charsWidth( aChars, aOffset, count );
        }
        while( count < aCount && width < aWidth );

        if( width <= aWidth )
        {
            return count;
        }

        do
        {
            count--;

            if( count < 0 )
            {
                count = 0;
            }

            width = aFont.charsWidth( aChars, aOffset, count );
        }
        while( 0 < count && width > aWidth );

        return count;
    }

    /**
     * Replaces unprintable characters (such as tabs and carriage returns) with spaces.
     * 
     * Note: This implementation is used for (and is meant for) short strings. It is more efficient to use 
     * {@link String#replace(char, char)} multiple times when you're not sure how short the string is.
     * 
     * @param aChars the input character array
     * @param aOffset beginning offset of the substring to consider
     * @param aDelta length of the substring to consider
     */
    private static void replaceUnprintableCharacters( char[] aChars, int aOffset, int aDelta )
    {
        for( int index = aOffset; index < aOffset + aDelta; ++index )
        {
            switch( aChars[index] )
            {
                case ASCII_TAB:
                case ASCII_CR:
                    // Replace these unprintable characters with spaces.
                    aChars[index] = ASCII_SPACE;
                    break;
            }
        }
    }

    /**
     * For single row content that is wider than the component's width, the minimum length (in characters) required 
     * before an 'ellipsis' is inserted at the end.
     */
    private static final int  MINIMUM_LENGTH_BEFORE_INSERTING_ELLIPSIS = 10;

    /**
     * {@link #textToRows(char[], int, Font, Vector)} named characters.
     */
    private static final char ASCII_CR                                 = 0x0D;
    private static final char ASCII_LF                                 = 0x0A;
    private static final char ASCII_TAB                                = 0x09;
    private static final char ASCII_SPACE                              = 0x20;
    private static final char ASCII_ELLIPSIS                           = 0x85;

    /**
     * Number of characters to 'hop' when attempting to calculate the best 'count' value.
     */
    private static final int  DELTA_FOR_SINGLE_ROW_COUNT               = 10;

    private boolean           iShouldIndicateErrors                    = false;
    private boolean           iErrorsOccurred;
}